﻿/// File: ViewletControls\scrollbar.fs
/// 
/// Author: Giacomo Righetti (righetti@di.unipi.it)
///   
/// ------------------------------------------------------------
/// Copyright (c) Microsoft Corporation.  All rights reserved.
/// 
/// The use and distribution terms for this software are 
/// contained in the file named license.txt, which can be found 
/// in the root of this distribution.
/// By using this software in any fashion, you are agreeing to 
/// be bound by the terms of this license.
///
/// You must not remove this notice, or any other, from this
/// software.
/// ------------------------------------------------------------

namespace LightweightControls

open System
open System.Drawing
open System.Drawing.Drawing2D
open System.Windows.Forms
open System.Windows.Forms.VisualStyles

///
/// Summary:
///     Enumeration for the states of a scroll bar.
type State =
    | DecrementArrowOver = 0
    | DecrementArrowClicked = 1
    | DecrementTrackClicked = 2
    | ThumbOver = 3
    | ThumbClicked = 4
    | IncrementArrowOver = 5
    | IncrementArrowClicked = 6
    | IncrementTrackClicked = 7
    | None = 8

///
/// Summary:
///     The abstract base class for the lightweight scrollbar control.
[<AbstractClass>]
type LightweightScrollbarBase() as x =
    inherit LightweightControl()
    
    let defaultMaximum = 100
    let defaultMinimum = 0
    let defaultValue = 0
        
    let defaultSmallChange = 1
    let defaultLargeChange = 10

    let _defaultArrowWidth = 20
    let _thumbWidth = 20
    
    let normalColors = Color.Black, Color.LightGray
    let hotColors = Color.Navy, Color.Aquamarine 
     
    let timer : Timer = new Timer(Interval = 100)  
    let mutable timerWasStarted = false  
    let mutable lastRect = Rectangle.Empty  
      
    let mutable _decrementArrow = Rectangle.Empty
    let mutable _incrementArrow = Rectangle.Empty
    let mutable _decrementTrack = Rectangle.Empty
    let mutable _incrementTrack = Rectangle.Empty
    let mutable _thumb = Rectangle.Empty
    
    let mutable maximum = defaultMaximum
    let mutable minimum = defaultMinimum
    let mutable value = defaultValue
    let mutable smallChange = defaultSmallChange
    let mutable largeChange = defaultLargeChange
    
    let mutable isMouseInside = false
    let mutable state = State.None 
    let mutable oldMousePos = Point.Empty
    
    let scrollEvent = Event<ScrollEventArgs>()
   
    do 
        x.Size <- x.getDefaultSize()
        x.updateBoundingRects()
        timer.Tick.Add(fun _ -> x.timerOnTick())
    
    ///
    /// Summary:
    ///     Gets or sets the upper limit of values of the scrollable range.
    ///
    /// Returns:
    ///     A numeric value. The default value is 100.                          
    member x.Maximum 
        with get() = maximum
        and set(v) = 
            if v <> maximum then
                if minimum > v then minimum <- v
                if v < value then value <- v
                maximum <- value
                base.Invalidate()
    
    ///
    /// Summary:
    ///     Gets or sets the lower limit of values of the scrollable range.
    ///
    /// Returns:
    ///     A numeric value. The default value is 0.    
    member x.Minimum
        with get() = minimum
        and set(v) = 
            if v <> minimum then
                if maximum < v then maximum <- v
                if v > value then value <- v
                minimum <- value
                base.Invalidate()
    
    ///
    /// Summary:
    ///     Gets or sets a numeric value that represents the current position of the
    ///     scroll box on the scroll bar control.
    ///
    /// Returns:
    ///     A numeric value that is within the Minimum and Maximum range. The default value is 0.
    ///
    /// Exceptions:
    ///   System.ArgumentOutOfRangeException:
    ///     The assigned value is less than the Minimum property value or the assigned value is 
    ///     greater than the Maximum property value.
    member x.Value
        with get() = value
        and set(v) =
            if v <> value then
                if v < minimum || v > maximum then
                    raise (ArgumentOutOfRangeException("The assigned value should be between Maximum and Minimum."))
                value <- v
                base.Invalidate()
    
    ///
    /// Summary:
    ///     Gets or sets the value to be added to or subtracted from the System.Windows.Forms.ScrollBar.Value
    ///     property when the scroll box is moved a small distance.
    ///
    /// Returns:
    ///     A numeric value. The default value is 1.
    ///
    /// Exceptions:
    ///   System.ArgumentOutOfRangeException:
    ///     The assigned value is less than 0.    
    member x.SmallChange
        with get() = smallChange
        and set(value) = 
            if value < 0 then
                raise (ArgumentOutOfRangeException("The assigned value must be greater than or equal to 0."))
            smallChange <- value
    
    ///
    /// Summary:
    ///     Gets or sets a value to be added to or subtracted from the Value
    ///     property when the scroll box is moved a large distance.
    ///
    /// Returns:
    ///     A numeric value. The default value is 10.
    ///
    /// Exceptions:
    ///   System.ArgumentOutOfRangeException:
    ///     The assigned value is less than 0.
    member x.LargeChange
        with get() = largeChange
        and set(value) = 
            if value < 0 then 
                raise (ArgumentOutOfRangeException("The assigned value must be greater than or equal to 0."))
            largeChange <- value
                  
    override x.Height
        with set(h) = 
            base.Height <- h
            x.updateBoundingRects()
            base.Invalidate()
            
    override x.Size
        with set(value) =
            base.Size <- value
            x.updateBoundingRects()
            base.Invalidate()        

    member internal x.decrementArrow
        with get() = _decrementArrow
        and set(v) = _decrementArrow <- v
        
    member internal x.incrementArrow
        with get() = _incrementArrow
        and set(v) = _incrementArrow <- v
        
    member internal x.decrementTrack
        with get() = _decrementTrack
        and set(v) = _decrementTrack <- v
    
    member internal x.incrementTrack
        with get() = _incrementTrack
        and set(v) = _incrementTrack <- v
    
    member internal x.thumb
        with get() = _thumb
        and set(v) = _thumb <- v

    member internal x.defaultArrowWidth = _defaultArrowWidth
    member internal x.thumbWidth = _thumbWidth

    member internal x.move d =
        let delta = if value + d > maximum then maximum - value
                    elif value + d < minimum then minimum - value
                    else d
        value <- value + delta
        x.updateArrowAndTrack (int (Math.Round(float delta * float (x.getUpdateDelta()))))
    
    // FIX: make variable based on the scrollbar size.
    member private x.drawThumb
        with get() = true
        
    member private x.moveIncrement() = x.move defaultSmallChange
    member private x.moveDecrement() = x.move -defaultSmallChange
    member private x.moveLargeIncrement() = x.move defaultLargeChange
    member private x.moveLargeDecrement() = x.move -defaultLargeChange    
    
    // FIX: refactor needed!
    member private x.timerOnTick() =
        if x.decrementTrack.Contains(oldMousePos) then
            x.moveLargeDecrement()
            scrollEvent.Trigger(ScrollEventArgs(ScrollEventType.LargeDecrement, x.Value))
                
        elif x.incrementTrack.Contains(oldMousePos) then
            x.moveLargeIncrement()
            scrollEvent.Trigger(ScrollEventArgs(ScrollEventType.LargeIncrement, x.Value))
                
        elif x.decrementArrow.Contains(oldMousePos) then 
            x.moveDecrement()
            scrollEvent.Trigger(ScrollEventArgs(ScrollEventType.SmallDecrement, x.Value))
                
        elif x.incrementArrow.Contains(oldMousePos) then
            x.moveIncrement()
            scrollEvent.Trigger(ScrollEventArgs(ScrollEventType.SmallIncrement, x.Value))
        
        base.Invalidate()
    
    abstract updateBoundingRects : unit -> unit
    abstract updateArrowAndTrack : int -> unit
    abstract updateThumb : Point -> unit
    
    abstract drawDecrementArrow : Graphics * Color * Color -> unit
    abstract drawIncrementArrow : Graphics * Color * Color -> unit
    
    abstract drawThumbFunc :          Graphics * Rectangle * ScrollBarState -> unit 
    abstract drawDecrementTrackFunc : Graphics * Rectangle * ScrollBarState -> unit
    abstract drawThumbGripFunc :      Graphics * Rectangle * ScrollBarState -> unit
    abstract drawIncrementTrackFunc : Graphics * Rectangle * ScrollBarState -> unit
    
    abstract getDecrementArrowState : State -> ScrollBarArrowButtonState
    abstract getIncrementArrowState : State -> ScrollBarArrowButtonState
    abstract getDisabledArrowStates : unit  -> ScrollBarArrowButtonState * ScrollBarArrowButtonState
    
    abstract getDefaultSize : unit -> Size
    abstract getUpdateDelta : unit -> float32
    
    override x.OnMouseEnter (e) =
        isMouseInside <- true
        base.Invalidate()

    override x.OnMouseLeave (e) = 
        state <- State.None
        isMouseInside <- false
        if timer.Enabled then
            timer.Stop()
            timerWasStarted <- true
            // FIX: refactor needed!
            if x.decrementTrack.Contains(oldMousePos) then
                lastRect <- x.decrementTrack
            elif x.incrementTrack.Contains(oldMousePos) then
                lastRect <- x.incrementTrack
            elif x.incrementArrow.Contains(oldMousePos) then
                lastRect <- x.incrementArrow
            elif x.decrementArrow.Contains(oldMousePos) then      
                lastRect <- x.decrementArrow
        base.Invalidate()
    
    override x.OnMouseMove(e) =
        let oldState = state
        if state = State.ThumbClicked then
            let d = Point(e.Location.X - oldMousePos.X, e.Location.Y - oldMousePos.Y)
            x.updateThumb d
            oldMousePos <- e.Location
            // repaint only decrementTrack, thumb & incrementTrack rectangles
            let invRect = Rectangle.Union(_decrementTrack, Rectangle.Union(_thumb, _incrementTrack))
            invRect.Offset(x.Position.X, x.Position.Y)
            base.Invalidate(invRect)
        else 
            // if the cursor enters inside the control after the timer was started ..
            if e.Button = MouseButtons.Left && timerWasStarted && lastRect.Contains(e.Location) then 
                timer.Start()
                timerWasStarted <- false
                
            elif e.Button = MouseButtons.None then
                if _decrementArrow.Contains(e.Location) then state <- State.DecrementArrowOver
                elif _incrementArrow.Contains(e.Location) then state <- State.IncrementArrowOver
                elif _thumb.Contains(e.Location) then state <- State.ThumbOver
                else state <- State.None
                
            if state <> oldState then 
                base.Invalidate()
    
    override x.OnMouseDown(e) =
        oldMousePos <- e.Location
        if x.thumb.Contains(e.Location) && e.Button = MouseButtons.Left then
            state <- State.ThumbClicked
            // repaint only thumb rectangle
            let invRect = _thumb
            invRect.Offset(x.Position.X, x.Position.Y)
            base.Invalidate(invRect)
        else            
            if x.decrementTrack.Contains(e.Location) then
                x.moveLargeDecrement()
                state <- State.DecrementTrackClicked
                scrollEvent.Trigger(ScrollEventArgs(ScrollEventType.LargeDecrement, x.Value))
                
            elif x.incrementTrack.Contains(e.Location) then
                x.moveLargeIncrement()
                state <- State.IncrementTrackClicked
                scrollEvent.Trigger(ScrollEventArgs(ScrollEventType.LargeIncrement, x.Value))
                
            elif state = State.DecrementArrowOver then
                x.moveDecrement()
                state <- State.DecrementArrowClicked
                scrollEvent.Trigger(ScrollEventArgs(ScrollEventType.SmallDecrement, x.Value))
                
            elif state = State.IncrementArrowOver then
                x.moveIncrement()
                state <- State.IncrementArrowClicked
                scrollEvent.Trigger(ScrollEventArgs(ScrollEventType.SmallIncrement, x.Value))
                
            timer.Start()
            base.Invalidate() // TODO: repaint only certain rectangles. eg: decrementArrow, decrementTrack, thumb, incrementTrack
                              //       or decrementTrack, thumb, incrementTrack, etc.
             
    override x.OnMouseUp (e) =       
        let oldState = state
        
        if x.decrementTrack.Contains(e.Location) then
            state <- State.None
        elif x.incrementTrack.Contains(e.Location) then
            state <- State.None
        elif x.incrementArrow.Contains(e.Location) then
            state <- State.IncrementArrowOver
        elif x.decrementArrow.Contains(e.Location) then      
            state <- State.DecrementArrowOver
        elif x.thumb.Contains(e.Location) then
            state <- State.ThumbOver
              
        timer.Stop()
        
        if state <> oldState then base.Invalidate()
                                          
    override x.OnPaint (e) =
        let g = e.Graphics
        
        if x.Enabled then
            if isMouseInside then
                let arrowState =
                    if state = State.DecrementArrowClicked then ScrollBarState.Pressed
                    elif state = State.DecrementArrowOver then ScrollBarState.Hot 
                    else ScrollBarState.Normal
                x.drawThumbFunc (g, _decrementArrow, arrowState)
                let c1, c2 = 
                    if arrowState = ScrollBarState.Normal then normalColors else hotColors
                x.drawDecrementArrow(g, c1, c2)
            else
                let arrowState = x.getDecrementArrowState state    
                ScrollBarRenderer.DrawArrowButton(g, _decrementArrow, arrowState)
                    
            let leftTrackState =
                if state = State.DecrementTrackClicked then ScrollBarState.Pressed
                else ScrollBarState.Normal
            x.drawDecrementTrackFunc(g, _decrementTrack, leftTrackState)
        
            let thumbState =
                if state = State.ThumbClicked then ScrollBarState.Pressed
                elif state = State.ThumbOver then ScrollBarState.Hot
                else ScrollBarState.Normal
            x.drawThumbFunc(g, _thumb, thumbState)
                
            if x.drawThumb then
                x.drawThumbGripFunc(g, _thumb, thumbState)
        
            let rightTrackState =
                if state = State.IncrementTrackClicked then ScrollBarState.Pressed
                else ScrollBarState.Normal
            x.drawIncrementTrackFunc(g, _incrementTrack, rightTrackState)
        
            if isMouseInside then   
                let arrowState = 
                    if state = State.IncrementArrowClicked then ScrollBarState.Pressed
                    elif state = State.IncrementArrowOver then ScrollBarState.Hot
                    else ScrollBarState.Normal
                x.drawThumbFunc(g, _incrementArrow, arrowState)
                let c1, c2 = 
                    if arrowState = ScrollBarState.Normal then normalColors else hotColors
                x.drawIncrementArrow(g, c1, c2)
            else
                let arrowState = x.getIncrementArrowState state
                ScrollBarRenderer.DrawArrowButton(g, _incrementArrow, arrowState)
            
        else // if x.Enabled then  
            ScrollBarRenderer.DrawArrowButton(g, _decrementArrow, fst (x.getDisabledArrowStates()))  
            // NB: it is not necessary to call drawDecrementTrackFunc.. it could be used drawIncrementTrackFunc instead
            x.drawDecrementTrackFunc(g, Rectangle.Union(_decrementTrack, _incrementTrack), ScrollBarState.Disabled)
            ScrollBarRenderer.DrawArrowButton(g, _incrementArrow, snd (x.getDisabledArrowStates()))
                
    ////////////
    // Events // 
    ////////////
            
    member x.Scroll = scrollEvent.Publish
    
///
/// Summary:
///     The lightweight version of the HScrollBar control.
type LWHorizontalScrollbar() as x =
    inherit LightweightScrollbarBase()

    override x.getDefaultSize() = Size(80,17)

    override x.getDecrementArrowState state = 
        match state with
        | State.DecrementArrowClicked -> ScrollBarArrowButtonState.LeftPressed
        | State.DecrementArrowOver -> ScrollBarArrowButtonState.LeftHot
        | _ -> ScrollBarArrowButtonState.LeftNormal

    override x.getIncrementArrowState state = 
        match state with
        | State.DecrementArrowClicked -> ScrollBarArrowButtonState.RightPressed
        | State.DecrementArrowOver -> ScrollBarArrowButtonState.RightHot
        | _ -> ScrollBarArrowButtonState.RightNormal

    override x.drawThumbFunc (g:Graphics, r:Rectangle, state:ScrollBarState) = 
        ScrollBarRenderer.DrawHorizontalThumb (g,r,state)
        
    override x.drawDecrementTrackFunc (g:Graphics, r:Rectangle, state:ScrollBarState) = 
        ScrollBarRenderer.DrawLeftHorizontalTrack (g,r,state)
    
    override x.drawThumbGripFunc (g:Graphics, r:Rectangle, state:ScrollBarState) = 
        ScrollBarRenderer.DrawHorizontalThumbGrip (g,r,state)
    
    override x.drawIncrementTrackFunc (g:Graphics, r:Rectangle, state:ScrollBarState) = 
        ScrollBarRenderer.DrawRightHorizontalTrack (g,r,state)

    override x.getDisabledArrowStates() = 
        ScrollBarArrowButtonState.LeftDisabled, ScrollBarArrowButtonState.RightDisabled
    
    override x.getUpdateDelta() = (float32 x.Size.Width - 2.0f * float32 x.defaultArrowWidth) / float32 (x.Maximum - x.Minimum)   

    override x.updateArrowAndTrack (delta) =
        x.decrementTrack <- Rectangle(x.decrementTrack.Location, Size(x.decrementTrack.Width + delta, x.decrementTrack.Height))
        x.thumb          <- Rectangle(x.thumb.X + delta, x.thumb.Y, x.thumb.Width, x.thumb.Height)
        x.incrementTrack <- Rectangle(x.incrementTrack.X + delta, x.incrementTrack.Y, x.incrementTrack.Width - delta, x.incrementTrack.Height)
                    
    override x.updateBoundingRects() =
        x.decrementArrow <- Rectangle(0, 0, x.defaultArrowWidth, x.Size.Height)
        x.incrementArrow <- Rectangle(x.Size.Width - x.defaultArrowWidth, 0, x.defaultArrowWidth, x.Size.Height)
        
        let updateDelta = (float32 x.Size.Width - 2.0f * float32 x.defaultArrowWidth) / float32 (x.Maximum - x.Minimum)
        x.thumb          <- Rectangle(x.decrementArrow.Width + int (Math.Round(float x.Value * float updateDelta)), 0, x.thumbWidth, x.Size.Height)
                
        x.decrementTrack <- Rectangle(x.defaultArrowWidth, 0, x.thumb.X - x.decrementArrow.Right, x.Size.Height)
        x.incrementTrack <- Rectangle(x.thumb.Right, 0, x.incrementArrow.X - x.thumb.Right, x.Size.Height)
                         
    override x.updateThumb d =
        x.move d.X
        
    override x.drawDecrementArrow (g, c1, c2) =
        let pts = [| Point(7, x.decrementArrow.Height / 2 - 1);
                     Point(x.decrementArrow.Width / 2 + 1, x.decrementArrow.Height / 2 - 5);
                     Point(x.decrementArrow.Width / 2 + 1, x.decrementArrow.Height / 2 + 3) |]
                     
        use brush = new LinearGradientBrush(Point(7, x.decrementArrow.Height / 2 - 2), 
                                            Point(x.decrementArrow.Width / 2 + 1, x.decrementArrow.Height / 2 - 1), 
                                            c1, c2)
        g.FillPolygon(brush, pts)
    
    override x.drawIncrementArrow (g, c1, c2) =
        let pts = [| Point(x.incrementArrow.X + 8, x.incrementArrow.Height / 2 - 5);
                     Point(x.incrementArrow.X + x.incrementArrow.Width / 2 + 2, x.incrementArrow.Height / 2 - 1);
                     Point(x.incrementArrow.X + 8, x.incrementArrow.Height / 2 + 3) |]
                     
        use brush = new LinearGradientBrush(Point(x.incrementArrow.X + 7, x.incrementArrow.Height / 2 - 5),
                                            Point(x.incrementArrow.X + x.incrementArrow.Width / 2 + 1, x.incrementArrow.Height / 2 + 3), 
                                            c1, c2)
        g.FillPolygon(brush, pts)

///
/// Summary:
///     The lightweight version of the VScrollBar control.
type LWVerticalScrollbar() as x =
    inherit LightweightScrollbarBase()
    
    override x.getDefaultSize() = Size(17,80)
    
    override x.getDecrementArrowState state = 
        match state with
        | State.DecrementArrowClicked -> ScrollBarArrowButtonState.UpPressed
        | State.DecrementArrowOver -> ScrollBarArrowButtonState.UpHot
        | _ -> ScrollBarArrowButtonState.UpNormal

    override x.getIncrementArrowState state = 
        match state with
        | State.DecrementArrowClicked -> ScrollBarArrowButtonState.DownPressed
        | State.DecrementArrowOver -> ScrollBarArrowButtonState.DownHot
        | _ -> ScrollBarArrowButtonState.DownNormal
    
    override x.drawThumbFunc (g:Graphics, r:Rectangle, state:ScrollBarState) = 
        ScrollBarRenderer.DrawVerticalThumb (g,r,state)
        
    override x.drawDecrementTrackFunc (g:Graphics, r:Rectangle, state:ScrollBarState) = 
        ScrollBarRenderer.DrawUpperVerticalTrack (g,r,state)
    
    override x.drawThumbGripFunc (g:Graphics, r:Rectangle, state:ScrollBarState) = 
        ScrollBarRenderer.DrawVerticalThumbGrip (g,r,state)
    
    override x.drawIncrementTrackFunc (g:Graphics, r:Rectangle, state:ScrollBarState) = 
        ScrollBarRenderer.DrawLowerVerticalTrack (g,r,state)
    
    override x.getDisabledArrowStates() = 
        ScrollBarArrowButtonState.UpDisabled, ScrollBarArrowButtonState.DownDisabled
        
    override x.getUpdateDelta() = (float32 x.Size.Height - 2.0f * float32 x.defaultArrowWidth) / float32 (x.Maximum - x.Minimum)
        
    override x.updateArrowAndTrack (delta) =
        x.decrementTrack <- Rectangle(x.decrementTrack.Location, Size(x.decrementTrack.Width, x.decrementTrack.Height + delta))
        x.thumb          <- Rectangle(x.thumb.X, x.thumb.Y + delta, x.thumb.Width, x.thumb.Height)
        x.incrementTrack <- Rectangle(x.incrementTrack.X, x.incrementTrack.Y + delta, x.incrementTrack.Width, x.incrementTrack.Height - delta)
                    
    override x.updateBoundingRects() =
        x.decrementArrow <- Rectangle(0, 0, x.Size.Width, x.defaultArrowWidth)
        x.incrementArrow <- Rectangle(0, x.Size.Height - x.defaultArrowWidth, x.Size.Width, x.defaultArrowWidth)
        
        let updateDelta = (x.Size.Height - 2 * x.defaultArrowWidth) / (x.Maximum - x.Minimum)
        x.thumb          <- Rectangle(0, x.decrementArrow.Height + updateDelta * x.Value, x.Size.Width, x.thumbWidth)
        
        x.decrementTrack <- Rectangle(0, x.defaultArrowWidth, x.Size.Width, x.thumb.Y - x.decrementArrow.Bottom)
        x.incrementTrack <- Rectangle(0, x.thumb.Bottom, x.Size.Width, x.incrementArrow.Y - x.thumb.Bottom)
                 
    override x.updateThumb d =
        x.move d.Y
                
    override x.drawDecrementArrow (g, c1, c2) =
        let pts = [| Point(x.decrementArrow.Width / 2 - 3, x.decrementArrow.Height / 2 + 1);
                     Point(x.decrementArrow.Width / 2,     x.decrementArrow.Height / 2 - 3);
                     Point(x.decrementArrow.Width / 2 + 4, x.decrementArrow.Height / 2 + 2) |]
                     
        use brush = new LinearGradientBrush(Point(x.decrementArrow.Width / 2 - 3, x.decrementArrow.Height / 2 - 3), 
                                            Point(x.decrementArrow.Width / 2 + 4, x.decrementArrow.Height / 2 + 2), 
                                            c1, c2)
        g.FillPolygon(brush, pts)
    
    override x.drawIncrementArrow (g, c1, c2) =
        let pts = [| Point(x.incrementArrow.Width / 2 - 3, x.incrementArrow.Y + x.incrementArrow.Height / 2 - 2);
                     Point(x.incrementArrow.Width / 2,     x.incrementArrow.Y + x.incrementArrow.Height / 2 + 2);
                     Point(x.incrementArrow.Width / 2 + 4, x.incrementArrow.Y + x.incrementArrow.Height / 2 - 2) |]
                     
        use brush = new LinearGradientBrush(Point(x.incrementArrow.Width / 2 - 3, x.incrementArrow.Y + x.incrementArrow.Height / 2 - 2), 
                                            Point(x.incrementArrow.Width / 2 + 4, x.incrementArrow.Y + x.incrementArrow.Height / 2 + 2), 
                                            c1, c2)
        g.FillPolygon(brush, pts)